package com.ffe.flower.engine.service.impl;

import java.util.List;
import java.util.stream.Collectors;

import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import com.ffe.flower.engine.component.ProcessEngine;
import com.ffe.flower.engine.constant.AuditStatus;
import com.ffe.flower.engine.dao.FlowInfoDao;
import com.ffe.flower.engine.dto.FlowCriteriaQuery;
import com.ffe.flower.engine.extend.IProcessCommonLogger;
import com.ffe.flower.engine.extend.IProcessLogger;
import com.ffe.flower.engine.model.FlowInfo;
import com.ffe.flower.engine.model.FlowInfo_;
import com.ffe.flower.engine.model.FlowStateDetail;
import com.ffe.flower.engine.service.IFlowInfoService;
import com.ffe.flower.engine.service.IFlowStateDetailService;
import com.ffe.flower.engine.wrapper.FlowStateWrapper;
import com.ffe.flower.engine.wrapper.FlowWrapper;

@Transactional
@Service
public class FlowInfoServiceImpl implements IFlowInfoService {

  @Autowired
  private IFlowStateDetailService flowStateDetailService;

  @Autowired
  private FlowInfoDao flowInfoDao;

  @Autowired
  private List<IProcessCommonLogger> processCommonLoggers;

  @Override
  public FlowWrapper start(Object field, String processId, String applicantId, String targetId, Enum<?> curFlow,
      String promoterId, boolean promoterAgent) {
    FlowInfo newFlowInfo = new FlowInfo();
    newFlowInfo.setApplicantId(applicantId);
    newFlowInfo.setTargetId(targetId);
    newFlowInfo.setPromoterId(promoterId);
    newFlowInfo.setPromoterAgent(promoterAgent);
    add(newFlowInfo);
    FlowStateDetail flowStateDetail = flowStateDetailService.create(newFlowInfo.getFlowId(), curFlow);
    newFlowInfo.setCurStateId(flowStateDetail.getFlowId());
    newFlowInfo.setCurState(flowStateDetail.getFlowState());
    add(newFlowInfo);

    {
      FlowWrapper newFlowWrapper = new FlowWrapper(newFlowInfo);
      FlowStateWrapper newStateWrapper = new FlowStateWrapper(flowStateDetail);
      for (IProcessCommonLogger commonLogger : processCommonLoggers) {
        commonLogger.startProcess(newFlowWrapper, newStateWrapper);
      }
      IProcessLogger procLogger = ProcessEngine.findLogger(newFlowInfo.getProcessId());
      if (procLogger != null) {
        procLogger.startProcess(field, newFlowWrapper, newStateWrapper);
      }
      return newFlowWrapper;
    }
  }

  @Override
  public FlowWrapper transit(Object field, Integer flowId, Enum<?> toState, String auditorId, boolean auditorAgent,
      String remark) {
    FlowInfo flowInfo = get(flowId);
    FlowStateDetail completeFlowStateDetail = flowStateDetailService.complete(flowInfo.getCurStateId(),
        AuditStatus.APPROVAL, auditorId, auditorAgent, remark);
    FlowStateDetail newFlowStateDetail = flowStateDetailService.create(flowId, toState);
    flowInfo.setCurStateId(newFlowStateDetail.getFlowId());
    flowInfo.setCurState(newFlowStateDetail.getFlowState());

    {
      FlowWrapper afterFlowWrapper = new FlowWrapper(flowInfo);
      FlowStateWrapper afterStateWrapper = new FlowStateWrapper(completeFlowStateDetail);
      FlowStateWrapper newStateWrapper = new FlowStateWrapper(newFlowStateDetail);
      for (IProcessCommonLogger commonLogger : processCommonLoggers) {
        commonLogger.transit(afterFlowWrapper, afterStateWrapper, newStateWrapper);
      }
      IProcessLogger procLogger = ProcessEngine.findLogger(flowInfo.getProcessId());
      if (procLogger != null) {
        procLogger.transit(field, afterFlowWrapper, afterStateWrapper, newStateWrapper);
      }

      return afterFlowWrapper;
    }
  }

  @Override
  public FlowWrapper transitToEnd(Object field, Integer flowId, Enum<?> toState, String auditorId, boolean auditorAgent,
      AuditStatus auditStatus, String remark, boolean throughout) {
    FlowInfo flowInfo = get(flowId);
    FlowStateDetail completeFlowStateDetail = flowStateDetailService.complete(flowInfo.getCurStateId(), auditStatus,
        auditorId, auditorAgent, remark);
    FlowStateDetail newFlowStateDetail = flowStateDetailService.create(flowId, toState);
    flowInfo.setCurStateId(newFlowStateDetail.getFlowId());
    flowInfo.setCurState(newFlowStateDetail.getFlowState());
    flowStateDetailService.complete(newFlowStateDetail.getFlowId(), AuditStatus.APPROVAL, auditorId, auditorAgent,
        remark);
    flowInfo.setThroughout(throughout);
    flowInfo.setFinished(true);

    {
      FlowWrapper afterFlowWrapper = new FlowWrapper(flowInfo);
      FlowStateWrapper afterStateWrapper = new FlowStateWrapper(completeFlowStateDetail);
      FlowStateWrapper newStateWrapper = new FlowStateWrapper(newFlowStateDetail);
      for (IProcessCommonLogger commonLogger : processCommonLoggers) {
        commonLogger.endProcess(afterFlowWrapper, afterStateWrapper, newStateWrapper);
      }
      IProcessLogger procLogger = ProcessEngine.findLogger(flowInfo.getProcessId());
      if (procLogger != null) {
        procLogger.endProcess(field, afterFlowWrapper, afterStateWrapper, newStateWrapper);
      }

      return afterFlowWrapper;
    }
  }

  @Override
  public FlowInfo add(FlowInfo info) {
    return flowInfoDao.save(info);
  }

  @Override
  public FlowInfo get(Integer flowId) {
    return flowInfoDao.findById(flowId).orElse(null);
  }

  @Override
  public long count(FlowCriteriaQuery criteriaQuery) {
    criteriaQuery.setPageRequest(null);
    return flowInfoDao.count(createSpecific(criteriaQuery));
  }

  @Override
  public List<FlowWrapper> query(FlowCriteriaQuery criteriaQuery) {
    if (criteriaQuery.getPageRequest() == null) {
      return flowInfoDao.findAll(createSpecific(criteriaQuery)).stream().map(FlowWrapper::new)
          .collect(Collectors.toList());
    } else {
      return flowInfoDao.findAll(createSpecific(criteriaQuery), criteriaQuery.getPageRequest()).map(FlowWrapper::new)
          .getContent();
    }
  }

  private Specification<FlowInfo> createSpecific(FlowCriteriaQuery criteriaQuery) {
    return (root, query, cb) -> {
      Predicate predicate = cb.conjunction();
      List<Expression<Boolean>> exps = predicate.getExpressions();

      if (criteriaQuery.getLastModifiedDateStart() != null) {
        exps.add(
            cb.greaterThanOrEqualTo(root.get(FlowInfo_.lastModifiedDate), criteriaQuery.getLastModifiedDateStart()));
      }

      if (criteriaQuery.getLastModifiedDateEnd() != null) {
        exps.add(cb.lessThan(root.get(FlowInfo_.lastModifiedDate), criteriaQuery.getLastModifiedDateEnd()));
      }

      if (criteriaQuery.getCreatedDateStart() != null) {
        exps.add(cb.greaterThanOrEqualTo(root.get(FlowInfo_.createdDate), criteriaQuery.getCreatedDateStart()));
      }

      if (criteriaQuery.getCreatedDateEnd() != null) {
        exps.add(cb.lessThan(root.get(FlowInfo_.createdDate), criteriaQuery.getCreatedDateEnd()));
      }

      if (criteriaQuery.getThroughout() != null) {
        exps.add(cb.equal(root.get(FlowInfo_.throughout), criteriaQuery.getThroughout()));
      }
      if (criteriaQuery.getFinished() != null) {
        exps.add(cb.equal(root.get(FlowInfo_.finished), criteriaQuery.getFinished()));
      }
      if (criteriaQuery.getPromoterAgent() != null) {
        exps.add(cb.equal(root.get(FlowInfo_.promoterAgent), criteriaQuery.getPromoterAgent()));
      }
      if (criteriaQuery.getCurState() != null) {
        exps.add(cb.equal(root.get(FlowInfo_.curState), criteriaQuery.getCurState()));
      }
      if (criteriaQuery.getCurStateId() != null) {
        exps.add(cb.equal(root.get(FlowInfo_.curStateId), criteriaQuery.getCurStateId()));
      }
      if (criteriaQuery.getFlowId() != null) {
        exps.add(cb.equal(root.get(FlowInfo_.flowId), criteriaQuery.getFlowId()));
      }
      if (!StringUtils.isEmpty(criteriaQuery.getProcessId())) {
        exps.add(cb.equal(root.get(FlowInfo_.processId), criteriaQuery.getProcessId()));
      }
      if (!StringUtils.isEmpty(criteriaQuery.getApplicantId())) {
        exps.add(cb.equal(root.get(FlowInfo_.applicantId), criteriaQuery.getApplicantId()));
      }
      if (!StringUtils.isEmpty(criteriaQuery.getTargetId())) {
        exps.add(cb.equal(root.get(FlowInfo_.targetId), criteriaQuery.getTargetId()));
      }
      if (!StringUtils.isEmpty(criteriaQuery.getPromoterId())) {
        exps.add(cb.equal(root.get(FlowInfo_.promoterId), criteriaQuery.getPromoterId()));
      }
      return predicate;
    };
  }

}
